/*
 * Copyright 2010 Christian Matzat and others
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package net.matzat.android.hiberdroid;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import net.matzat.android.hiberdroid.exception.MappingNotFoundException;
import net.matzat.android.hiberdroid.exception.PropertyAccessException;
import net.matzat.android.hiberdroid.mapping.ColumnMapping;
import net.matzat.android.hiberdroid.mapping.EntityMapping;

import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * HiberDroidManager class manages the database connection to the SQLite database.
 */
public class HiberDroidManager {

    private final static String TAG = HiberDroidManager.class.getSimpleName();
    private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    private HiberDroidOpenHelper dbo = null;
    private static HiberDroidManager instance = null;

    private HiberDroidManager() {
        // do not allow direct instantiation
    }

    /**
     * returns a SQLiteDatabase instance to perform read/write operations on the database.
     *
     * @return SQLiteDatabase instance
     */
    private SQLiteDatabase getWritableDatabaseConnection() {
        if (!isInitialized()) {
            throw new IllegalStateException("Database ist not initialized.");
        }
        return dbo.getWritableDatabase();
    }

    /**
     * returns a SQLiteDatabase instance to perform only read operations on the database.
     *
     * @return SQLiteDatabase instance
     */
    private SQLiteDatabase getDatabaseConnection() {
        if (!isInitialized()) {
            throw new IllegalStateException("Database ist not initialized.");
        }
        return dbo.getReadableDatabase();
    }

    /**
     * get an instance of the HiberDroidManager. This instance is created only once and then shared.
     *
     * @return DBManager instance
     */
    public static synchronized HiberDroidManager getInstance() {
        if (instance == null) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Creating new HiberDroidManager.");
            }
            instance = new HiberDroidManager();
        }
        return instance;
    }

    /**
     * Initializes the database mapping.
     *
     * @param context        the Android context of the app
     * @param dbName         the database name
     * @param dbVersion      the database version
     * @param mappingClasses full qualified class names of the mapping classes as Strings (vararg)
     */
    public void initialize(Context context, String dbName, int dbVersion, String... mappingClasses) {
        if (dbo == null) {
            try {
                Configuration.createInstance(mappingClasses);
                dbo = new HiberDroidOpenHelper(context, dbName, dbVersion);
            } catch (ClassNotFoundException e) {
                Log.e(TAG, "Mapping Class not found", e);
                throw new MappingNotFoundException("Mapping class not found.");
            } catch (InstantiationException e) {
                Log.e(TAG, "Configuration was already configured", e);
                throw new MappingNotFoundException("Duplicate initialization call.");
            }
        }
    }

    /**
     * destroys the database connection and the mapping configuration
     */
    public void destroy() {
        dbo.close();
        dbo = null;
        Configuration.destroy();
    }

    /**
     * check if database mapping in initialized and a database connection object is available.
     *
     * @return true if successfully initialized
     */
    public boolean isInitialized() {
        return dbo != null;
    }

    /**
     * gets a Cursor for a database query.
     *
     * @param table         the database table
     * @param columns       an array of column names
     * @param selection     the where clause
     * @param selectionArgs the parameters for the selection arguments
     * @param group         the group clause
     * @param having        the having clause
     * @param order         the order clause
     * @return Cursor
     */
    public Cursor getCursor(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String group, String having, String order, String limit) {
        return getDatabaseConnection().query(distinct, table, columns, selection, selectionArgs, group, having, order, limit);
    }

    /**
     * saves a mapped entity object to the database. checks if the entity is new and performs an insert or update if
     * object is updated. requires an id column set for the entity to delete.
     *
     * @param entity
     */
    public void save(Object entity) {
        try {
            EntityMapping entityMapping = Configuration.findEntityMappingByClass(entity.getClass());
            if (entityMapping.getIdProperty() == null) {
                throw new MappingNotFoundException("cannot save without id column.");
            }
            boolean insert = true;
            String idValue = "";
            ContentValues contentValues = new ContentValues(entityMapping.getColumns().size());
            for (String property : entityMapping.getColumns().keySet()) {
                Field field = entity.getClass().getDeclaredField(property);
                field.setAccessible(true);
                ColumnMapping columnMapping = entityMapping.getColumns().get(property);
                Object value = null;
                if (field.get(entity) != null) {
                    value = field.get(entity);
                }
                if (columnMapping.isId()) {
                    idValue = value.toString();
                    if (idValue != null && !idValue.equals("0")) {
                        insert = false;
                    }
                } else {
                    switch (columnMapping.getColumnType()) {
                        case DATE:
                            value = value != null && value instanceof Date ? DATE_FORMAT.format(value) : null;
                            contentValues.put(columnMapping.getColumn(), value != null && value instanceof String ? (String) value : null);
                            break;
                        case STRING:
                            contentValues.put(columnMapping.getColumn(), value != null && value instanceof String ? (String) value : null);
                            break;
                        case BYTE:
                            contentValues.put(columnMapping.getColumn(), value != null && value instanceof Byte ? (Byte) value : null);
                            break;
                        case SHORT:
                            contentValues.put(columnMapping.getColumn(), value != null && value instanceof Short ? (Short) value : null);
                            break;
                        case INTEGER:
                            contentValues.put(columnMapping.getColumn(), value != null && value instanceof Integer ? (Integer) value : null);
                            break;
                        case LONG:
                            contentValues.put(columnMapping.getColumn(), value != null && value instanceof Long ? (Long) value : null);
                            break;
                        case DOUBLE:
                            contentValues.put(columnMapping.getColumn(), value != null && value instanceof Double ? (Double) value : null);
                            break;
                        case FLOAT:
                            contentValues.put(columnMapping.getColumn(), value != null && value instanceof Float ? (Float) value : null);
                            break;
                        case BLOB:
                            contentValues.put(columnMapping.getColumn(), value != null && value instanceof byte[] ? (byte[]) value : null);
                            break;
                        case BOOLEAN:
                            if (value != null && value instanceof Boolean) {
                                value = (Boolean) value ? new Integer(1) : new Integer(0);
                            }
                            contentValues.put(columnMapping.getColumn(), value != null && value instanceof Integer ? (Integer) value : null);
                            break;
                    }
                }
            }
            if (insert) {
                Long newIdValue = getWritableDatabaseConnection().insert(entityMapping.getTable(), null, contentValues);
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, String.format("entity with id %s was inserted", newIdValue.toString()));
                }
            } else {
                getWritableDatabaseConnection().update(entityMapping.getTable(), contentValues, entityMapping.getIdColumn() + "=?", new String[]{idValue});
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, String.format("entity with id %s was updated", idValue));
                }
            }
        } catch (NoSuchFieldException e) {
            throw new PropertyAccessException("property access failed.");
        } catch (IllegalAccessException e) {
            throw new PropertyAccessException("property access failed.");
        }
    }

    /**
     * deletes a mapped entity from the database. requires an id column set for the entity to delete.
     *
     * @param entity the entity object to delete
     */
    public void delete(Object entity) {
        EntityMapping entityMapping = Configuration.findEntityMappingByClass(entity.getClass());
        if (entityMapping.getIdProperty() == null) {
            throw new MappingNotFoundException("cannot delete without id column.");
        }
        try {
            Field field = entity.getClass().getDeclaredField(entityMapping.getIdProperty());
            field.setAccessible(true);
            Long value = null;
            if (field.get(entity) != null) {
                value = (Long) field.get(entity);
            }
            delete(entity.getClass(), value);
        } catch (NoSuchFieldException e) {
            throw new PropertyAccessException("property access failed.");
        } catch (IllegalAccessException e) {
            throw new PropertyAccessException("property access failed.");
        }
    }

    /**
     * deletes a mapped entity with the given id from the database. requires an id column set for the entity to delete.
     *
     * @param entityClass the entity class
     * @param entityId the entity id to delete
     */
    public void delete(Class entityClass, Long entityId) {
        EntityMapping entityMapping = Configuration.findEntityMappingByClass(entityClass);
        if (entityMapping.getIdProperty() == null) {
            throw new MappingNotFoundException("cannot delete without id column.");
        }
            if (entityId != null) {
                getWritableDatabaseConnection().delete(entityMapping.getTable(), entityMapping.getIdColumn() + "=?", new String[]{entityId.toString()});
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, String.format("entity with id %s was deleted", entityId.toString()));
                }
            }
    }

}

